/*
 * Copyright (c) 1999, 2008, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */
package javax.swing.text;

import java.util.*;
import java.awt.*;
import java.text.AttributedCharacterIterator;
import java.text.BreakIterator;
import java.awt.font.*;
import java.awt.geom.AffineTransform;
import javax.swing.JComponent;
import javax.swing.event.DocumentEvent;
import sun.font.BidiUtils;

/**
 * A flow strategy that uses java.awt.font.LineBreakMeasureer to
 * produce java.awt.font.TextLayout for i18n capable rendering.
 * If the child view being placed into the flow is of type
 * GlyphView and can be rendered by TextLayout, a GlyphPainter
 * that uses TextLayout is plugged into the GlyphView.
 *
 * @author Timothy Prinzing
 */
class TextLayoutStrategy extends FlowView.FlowStrategy {

  /**
   * Constructs a layout strategy for paragraphs based
   * upon java.awt.font.LineBreakMeasurer.
   */
  public TextLayoutStrategy() {
    text = new AttributedSegment();
  }

  // --- FlowStrategy methods --------------------------------------------

  /**
   * Gives notification that something was inserted into the document
   * in a location that the given flow view is responsible for.  The
   * strategy should update the appropriate changed region (which
   * depends upon the strategy used for repair).
   *
   * @param e the change information from the associated document
   * @param alloc the current allocation of the view inside of the insets. This value will be null
   * if the view has not yet been displayed.
   * @see View#insertUpdate
   */
  public void insertUpdate(FlowView fv, DocumentEvent e, Rectangle alloc) {
    sync(fv);
    super.insertUpdate(fv, e, alloc);
  }

  /**
   * Gives notification that something was removed from the document
   * in a location that the given flow view is responsible for.
   *
   * @param e the change information from the associated document
   * @param alloc the current allocation of the view inside of the insets.
   * @see View#removeUpdate
   */
  public void removeUpdate(FlowView fv, DocumentEvent e, Rectangle alloc) {
    sync(fv);
    super.removeUpdate(fv, e, alloc);
  }

  /**
   * Gives notification from the document that attributes were changed
   * in a location that this view is responsible for.
   *
   * @param changes the change information from the associated document
   * @param a the current allocation of the view
   * @param f the factory to use to rebuild if the view has children
   * @see View#changedUpdate
   */
  public void changedUpdate(FlowView fv, DocumentEvent e, Rectangle alloc) {
    sync(fv);
    super.changedUpdate(fv, e, alloc);
  }

  /**
   * Does a a full layout on the given View.  This causes all of
   * the rows (child views) to be rebuilt to match the given
   * constraints for each row.  This is called by a FlowView.layout
   * to update the child views in the flow.
   *
   * @param fv the view to reflow
   */
  public void layout(FlowView fv) {
    super.layout(fv);
  }

  /**
   * Creates a row of views that will fit within the
   * layout span of the row.  This is implemented to execute the
   * superclass functionality (which fills the row with child
   * views or view fragments) and follow that with bidi reordering
   * of the unidirectional view fragments.
   *
   * @param row the row to fill in with views.  This is assumed to be empty on entry.
   * @param pos The current position in the children of this views element from which to start.
   * @return the position to start the next row
   */
  protected int layoutRow(FlowView fv, int rowIndex, int p0) {
    int p1 = super.layoutRow(fv, rowIndex, p0);
    View row = fv.getView(rowIndex);
    Document doc = fv.getDocument();
    Object i18nFlag = doc.getProperty(AbstractDocument.I18NProperty);
    if ((i18nFlag != null) && i18nFlag.equals(Boolean.TRUE)) {
      int n = row.getViewCount();
      if (n > 1) {
        AbstractDocument d = (AbstractDocument) fv.getDocument();
        Element bidiRoot = d.getBidiRootElement();
        byte[] levels = new byte[n];
        View[] reorder = new View[n];

        for (int i = 0; i < n; i++) {
          View v = row.getView(i);
          int bidiIndex = bidiRoot.getElementIndex(v.getStartOffset());
          Element bidiElem = bidiRoot.getElement(bidiIndex);
          levels[i] = (byte) StyleConstants.getBidiLevel(bidiElem.getAttributes());
          reorder[i] = v;
        }

        BidiUtils.reorderVisually(levels, reorder);
        row.replace(0, n, reorder);
      }
    }
    return p1;
  }

  /**
   * Adjusts the given row if possible to fit within the
   * layout span.  Since all adjustments were already
   * calculated by the LineBreakMeasurer, this is implemented
   * to do nothing.
   *
   * @param r the row to adjust to the current layout span.
   * @param desiredSpan the current layout span >= 0
   * @param x the location r starts at.
   */
  protected void adjustRow(FlowView fv, int rowIndex, int desiredSpan, int x) {
  }

  /**
   * Creates a unidirectional view that can be used to represent the
   * current chunk.  This can be either an entire view from the
   * logical view, or a fragment of the view.
   *
   * @param fv the view holding the flow
   * @param startOffset the start location for the view being created
   * @param spanLeft the about of span left to fill in the row
   * @param rowIndex the row the view will be placed into
   */
  protected View createView(FlowView fv, int startOffset, int spanLeft, int rowIndex) {
    // Get the child view that contains the given starting position
    View lv = getLogicalView(fv);
    View row = fv.getView(rowIndex);
    boolean requireNextWord = (viewBuffer.size() == 0) ? false : true;
    int childIndex = lv.getViewIndex(startOffset, Position.Bias.Forward);
    View v = lv.getView(childIndex);

    int endOffset = getLimitingOffset(v, startOffset, spanLeft, requireNextWord);
    if (endOffset == startOffset) {
      return null;
    }

    View frag;
    if ((startOffset == v.getStartOffset()) && (endOffset == v.getEndOffset())) {
      // return the entire view
      frag = v;
    } else {
      // return a unidirectional fragment.
      frag = v.createFragment(startOffset, endOffset);
    }

    if ((frag instanceof GlyphView) && (measurer != null)) {
      // install a TextLayout based renderer if the view is responsible
      // for glyphs.  If the view represents a tab, the default
      // glyph painter is used (may want to handle tabs differently).
      boolean isTab = false;
      int p0 = frag.getStartOffset();
      int p1 = frag.getEndOffset();
      if ((p1 - p0) == 1) {
        // check for tab
        Segment s = ((GlyphView) frag).getText(p0, p1);
        char ch = s.first();
        if (ch == '\t') {
          isTab = true;
        }
      }
      TextLayout tl = (isTab) ? null :
          measurer.nextLayout(spanLeft, text.toIteratorIndex(endOffset),
              requireNextWord);
      if (tl != null) {
        ((GlyphView) frag).setGlyphPainter(new GlyphPainter2(tl));
      }
    }
    return frag;
  }

  /**
   * Calculate the limiting offset for the next view fragment.
   * At most this would be the entire view (i.e. the limiting
   * offset would be the end offset in that case).  If the range
   * contains a tab or a direction change, that will limit the
   * offset to something less.  This value is then fed to the
   * LineBreakMeasurer as a limit to consider in addition to the
   * remaining span.
   *
   * @param v the logical view representing the starting offset.
   * @param startOffset the model location to start at.
   */
  int getLimitingOffset(View v, int startOffset, int spanLeft, boolean requireNextWord) {
    int endOffset = v.getEndOffset();

    // check for direction change
    Document doc = v.getDocument();
    if (doc instanceof AbstractDocument) {
      AbstractDocument d = (AbstractDocument) doc;
      Element bidiRoot = d.getBidiRootElement();
      if (bidiRoot.getElementCount() > 1) {
        int bidiIndex = bidiRoot.getElementIndex(startOffset);
        Element bidiElem = bidiRoot.getElement(bidiIndex);
        endOffset = Math.min(bidiElem.getEndOffset(), endOffset);
      }
    }

    // check for tab
    if (v instanceof GlyphView) {
      Segment s = ((GlyphView) v).getText(startOffset, endOffset);
      char ch = s.first();
      if (ch == '\t') {
        // if the first character is a tab, create a dedicated
        // view for just the tab
        endOffset = startOffset + 1;
      } else {
        for (ch = s.next(); ch != Segment.DONE; ch = s.next()) {
          if (ch == '\t') {
            // found a tab, don't include it in the text
            endOffset = startOffset + s.getIndex() - s.getBeginIndex();
            break;
          }
        }
      }
    }

    // determine limit from LineBreakMeasurer
    int limitIndex = text.toIteratorIndex(endOffset);
    if (measurer != null) {
      int index = text.toIteratorIndex(startOffset);
      if (measurer.getPosition() != index) {
        measurer.setPosition(index);
      }
      limitIndex = measurer.nextOffset(spanLeft, limitIndex, requireNextWord);
    }
    int pos = text.toModelPosition(limitIndex);
    return pos;
  }

  /**
   * Synchronize the strategy with its FlowView.  Allows the strategy
   * to update its state to account for changes in that portion of the
   * model represented by the FlowView.  Also allows the strategy
   * to update the FlowView in response to these changes.
   */
  void sync(FlowView fv) {
    View lv = getLogicalView(fv);
    text.setView(lv);

    Container container = fv.getContainer();
    FontRenderContext frc = sun.swing.SwingUtilities2.
        getFontRenderContext(container);
    BreakIterator iter;
    Container c = fv.getContainer();
    if (c != null) {
      iter = BreakIterator.getLineInstance(c.getLocale());
    } else {
      iter = BreakIterator.getLineInstance();
    }

    Object shaper = null;
    if (c instanceof JComponent) {
      shaper = ((JComponent) c).getClientProperty(
          TextAttribute.NUMERIC_SHAPING);
    }
    text.setShaper(shaper);

    measurer = new LineBreakMeasurer(text, iter, frc);

    // If the children of the FlowView's logical view are GlyphViews, they
    // need to have their painters updated.
    int n = lv.getViewCount();
    for (int i = 0; i < n; i++) {
      View child = lv.getView(i);
      if (child instanceof GlyphView) {
        int p0 = child.getStartOffset();
        int p1 = child.getEndOffset();
        measurer.setPosition(text.toIteratorIndex(p0));
        TextLayout layout
            = measurer.nextLayout(Float.MAX_VALUE,
            text.toIteratorIndex(p1), false);
        ((GlyphView) child).setGlyphPainter(new GlyphPainter2(layout));
      }
    }

    // Reset measurer.
    measurer.setPosition(text.getBeginIndex());

  }

  // --- variables -------------------------------------------------------

  private LineBreakMeasurer measurer;
  private AttributedSegment text;

  /**
   * Implementation of AttributedCharacterIterator that supports
   * the GlyphView attributes for rendering the glyphs through a
   * TextLayout.
   */
  static class AttributedSegment extends Segment implements AttributedCharacterIterator {

    AttributedSegment() {
    }

    View getView() {
      return v;
    }

    void setView(View v) {
      this.v = v;
      Document doc = v.getDocument();
      int p0 = v.getStartOffset();
      int p1 = v.getEndOffset();
      try {
        doc.getText(p0, p1 - p0, this);
      } catch (BadLocationException bl) {
        throw new IllegalArgumentException("Invalid view");
      }
      first();
    }

    /**
     * Get a boundary position for the font.
     * This is implemented to assume that two fonts are
     * equal if their references are equal (i.e. that the
     * font came from a cache).
     *
     * @return the location in model coordinates.  This is not the same as the Segment coordinates.
     */
    int getFontBoundary(int childIndex, int dir) {
      View child = v.getView(childIndex);
      Font f = getFont(childIndex);
      for (childIndex += dir; (childIndex >= 0) && (childIndex < v.getViewCount());
          childIndex += dir) {
        Font next = getFont(childIndex);
        if (next != f) {
          // this run is different
          break;
        }
        child = v.getView(childIndex);
      }
      return (dir < 0) ? child.getStartOffset() : child.getEndOffset();
    }

    /**
     * Get the font at the given child index.
     */
    Font getFont(int childIndex) {
      View child = v.getView(childIndex);
      if (child instanceof GlyphView) {
        return ((GlyphView) child).getFont();
      }
      return null;
    }

    int toModelPosition(int index) {
      return v.getStartOffset() + (index - getBeginIndex());
    }

    int toIteratorIndex(int pos) {
      return pos - v.getStartOffset() + getBeginIndex();
    }

    private void setShaper(Object shaper) {
      this.shaper = shaper;
    }

    // --- AttributedCharacterIterator methods -------------------------

    /**
     * Returns the index of the first character of the run
     * with respect to all attributes containing the current character.
     */
    public int getRunStart() {
      int pos = toModelPosition(getIndex());
      int i = v.getViewIndex(pos, Position.Bias.Forward);
      View child = v.getView(i);
      return toIteratorIndex(child.getStartOffset());
    }

    /**
     * Returns the index of the first character of the run
     * with respect to the given attribute containing the current character.
     */
    public int getRunStart(AttributedCharacterIterator.Attribute attribute) {
      if (attribute instanceof TextAttribute) {
        int pos = toModelPosition(getIndex());
        int i = v.getViewIndex(pos, Position.Bias.Forward);
        if (attribute == TextAttribute.FONT) {
          return toIteratorIndex(getFontBoundary(i, -1));
        }
      }
      return getBeginIndex();
    }

    /**
     * Returns the index of the first character of the run
     * with respect to the given attributes containing the current character.
     */
    public int getRunStart(Set<? extends Attribute> attributes) {
      int index = getBeginIndex();
      Object[] a = attributes.toArray();
      for (int i = 0; i < a.length; i++) {
        TextAttribute attr = (TextAttribute) a[i];
        index = Math.max(getRunStart(attr), index);
      }
      return Math.min(getIndex(), index);
    }

    /**
     * Returns the index of the first character following the run
     * with respect to all attributes containing the current character.
     */
    public int getRunLimit() {
      int pos = toModelPosition(getIndex());
      int i = v.getViewIndex(pos, Position.Bias.Forward);
      View child = v.getView(i);
      return toIteratorIndex(child.getEndOffset());
    }

    /**
     * Returns the index of the first character following the run
     * with respect to the given attribute containing the current character.
     */
    public int getRunLimit(AttributedCharacterIterator.Attribute attribute) {
      if (attribute instanceof TextAttribute) {
        int pos = toModelPosition(getIndex());
        int i = v.getViewIndex(pos, Position.Bias.Forward);
        if (attribute == TextAttribute.FONT) {
          return toIteratorIndex(getFontBoundary(i, 1));
        }
      }
      return getEndIndex();
    }

    /**
     * Returns the index of the first character following the run
     * with respect to the given attributes containing the current character.
     */
    public int getRunLimit(Set<? extends Attribute> attributes) {
      int index = getEndIndex();
      Object[] a = attributes.toArray();
      for (int i = 0; i < a.length; i++) {
        TextAttribute attr = (TextAttribute) a[i];
        index = Math.min(getRunLimit(attr), index);
      }
      return Math.max(getIndex(), index);
    }

    /**
     * Returns a map with the attributes defined on the current
     * character.
     */
    public Map<Attribute, Object> getAttributes() {
      Object[] ka = keys.toArray();
      Hashtable<Attribute, Object> h = new Hashtable<Attribute, Object>();
      for (int i = 0; i < ka.length; i++) {
        TextAttribute a = (TextAttribute) ka[i];
        Object value = getAttribute(a);
        if (value != null) {
          h.put(a, value);
        }
      }
      return h;
    }

    /**
     * Returns the value of the named attribute for the current character.
     * Returns null if the attribute is not defined.
     *
     * @param attribute the key of the attribute whose value is requested.
     */
    public Object getAttribute(AttributedCharacterIterator.Attribute attribute) {
      int pos = toModelPosition(getIndex());
      int childIndex = v.getViewIndex(pos, Position.Bias.Forward);
      if (attribute == TextAttribute.FONT) {
        return getFont(childIndex);
      } else if (attribute == TextAttribute.RUN_DIRECTION) {
        return
            v.getDocument().getProperty(TextAttribute.RUN_DIRECTION);
      } else if (attribute == TextAttribute.NUMERIC_SHAPING) {
        return shaper;
      }
      return null;
    }

    /**
     * Returns the keys of all attributes defined on the
     * iterator's text range. The set is empty if no
     * attributes are defined.
     */
    public Set<Attribute> getAllAttributeKeys() {
      return keys;
    }

    View v;

    static Set<Attribute> keys;

    static {
      keys = new HashSet<Attribute>();
      keys.add(TextAttribute.FONT);
      keys.add(TextAttribute.RUN_DIRECTION);
      keys.add(TextAttribute.NUMERIC_SHAPING);
    }

    private Object shaper = null;
  }

}
